package Dungeon;


import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import be.kuleuven.cs.som.annotate.Model;

import ConditionPackage.Condition;
import ConditionPackage.RockCondition;
import SquarePackage.Direction;
import SquarePackage.Square;

/**
 * Class for a composed dungeon. This is a dungeon with subdungeons in it.
 * 
 * @invar	All coordinates are positive and smaller than the maximum value.
 * 			| isValidDimension()
 * 
 * @author 	Brecht Reynders en Florian Hendrickx
 * @version 1.0
 */
public class ComposedDungeon<E extends Square, Z extends E, T extends Subdungeon<Z>> extends Subdungeon<E> {
	
	////////////////////////
	//////CONTRUCTORS///////
	////////////////////////
	/**
	 * Initializes a new dungeon with the given coordinates.
	 * 
	 * @param	maxX
	 * 			The maximum x-coordinate value.
	 * @param	maxY
	 * 			The maximum y-coordinate value.
	 * @param	maxZ
	 * 			The maximum z-coordinate value.
	 * @effect	The given coordinates are the coordinates of the maximum dimension.
	 * 			| super(maxX, maxY, maxZ) 
	 */
	public ComposedDungeon(long maxX, long maxY, long maxZ){
		super(maxX, maxY, maxZ);
		this.dungeons = new HashMap<Dimension, T>();
	}
		
	/**
	 * Initializes a new dungeon with the given dimension as maximum dimension.
	 * 
	 * @param 	maxDimension
	 * 			The maximum dimension
	 * @effect	The given maximum dimension is the new max dimension.
	 * 			| super(maxDimension)
	 */
	public ComposedDungeon(Dimension maxDimension){
		super(maxDimension);
		this.dungeons = new HashMap<Dimension,T>();
	}
	
	///////////////////////
	///////VARIABLES///////
	///////////////////////
	/**
	 * Variable registering the dungeons in this composed dungeon and their starting position.
	 */
	private Map<Dimension, T> dungeons;
	
	////////////////////////
	//////DUNGEON PART//////
	////////////////////////
	/**
	 * Returns the dungeons and their starting dimensions.
	 */
	public Map<Dimension,T> getDungeons(){
		return dungeons;
	}
	
	/**
	 * Returns all the starting dimensions of the dimensions of this composed dungeon.
	 */
	public Set<Dimension> getDimensions(){
		return dungeons.keySet();
	}
	
	/**
	 * Returns all the dungeons in this composed dungeon.
	 */
	public Collection<T> getAllSubdungeons(){
		return getDungeons().values();
	}
	
	/**
	 * Returns all the levels and shafts.
	 */
	@Override
	public Collection<Dungeon<? extends E>> getAllLevelsAndShafts(){
		Set<Dungeon<? extends E>> levelsAndShafts = new HashSet<Dungeon<? extends E>>();
		for(T subdungeon : getAllSubdungeons()){
			if(subdungeon instanceof Dungeon)
				levelsAndShafts.addAll((subdungeon).getAllLevelsAndShafts());
		}
		return levelsAndShafts;
	}
	
	/**
	 * Adds a subdungeon to this dungeon.
	 * 
	 * @param 	subdungeon
	 * 			The dungeon to be added to this dungeon.
	 * @post	This dungeon containst now the given subdungeon.
	 * 			| this.getSubdungeons().contains(subdungeon)
	 */
	public <V extends Subdungeon<? extends Z>> void addDungeon(V subdungeon, Dimension dimension)throws IllegalArgumentException{
		if(subdungeon.hasContainingDungeon())
			throw new IllegalArgumentException("The given subdungeon already is in a composed dungeon.");
		if(!fit(dimension, (T) subdungeon))
			throw new IllegalArgumentException(dimension + " is not a valid place to add dungeon " + subdungeon);
		dungeons.put(dimension, (T) subdungeon);
		subdungeon.setContainingDungeon( (ComposedDungeon<?, ?, Subdungeon<?>>) this, dimension);
		mergeDungeon((T) subdungeon);
	}
	
	/**
	 * Merges the given dungeon with the other dungeons.
	 * 
	 * @param 	subdungeon 
	 * 			The added subdungeon.
	 * @post	If there is a square at the border of the given dungeon, than the square gets merged with the square at the other side of the border.
	 * 			| mergeSquares(dimension, Direction.? , subdungeon)
	 */
	public void mergeDungeon(T subdungeon){
		for(Dimension dimension: subdungeon.getUsedDimensions()){
			if(dimension.getXPosition()==0){
				mergeSquares(dimension,Direction.WEST,subdungeon);
			}
			if(dimension.getXPosition()==subdungeon.getMaxDimension().getXPosition()){
				mergeSquares(dimension,Direction.EAST,subdungeon);
			}
			if(dimension.getYPosition()==0){
				mergeSquares(dimension,Direction.NORTH,subdungeon);
			}
			if(dimension.getYPosition()==subdungeon.getMaxDimension().getYPosition()){
				mergeSquares(dimension,Direction.SOUTH,subdungeon);
			}
			if(dimension.getZPosition()==0){
				mergeSquares(dimension,Direction.FLOOR,subdungeon);
			}
			if(dimension.getZPosition()==subdungeon.getMaxDimension().getZPosition()){
				mergeSquares(dimension,Direction.CEILING,subdungeon);
			}
		}
	}

	/**
	 * Merges the square at the given dimension with the square at the given direction.
	 * 
	 * @param 	dimension
	 * 			The dimension on which there is a square in the subdungeon.
	 * @param 	direction
	 * 			Direction in which to search the other square.
	 * @param 	subdungeon
	 * 			The subdungeon that contains the given dimension.
	 * @post	The squares share the same border.
	 * 			| subdungeon.getSquareAt(dimension).getBorderAt(direction).equals(getRootDungeon().getSquareAt(subdungeon.getRootDimension(dimension).getDimensionIn(direction.opposite())).getBorderAt(direction.opposite()))
	 * @post	If the border that gets shared has no isolation, the rooms get external merged.
	 * 			| getRootDungeon().getRoomOf(other).externalMergeWith(getRootDungeon().getRoomOf(square))
	 */
	private void mergeSquares(Dimension dimension, Direction direction, T subdungeon){
		E square = subdungeon.getSquareAt(dimension);
		Dimension absoluteDimension = subdungeon.getRootDimension(dimension);
		if(absoluteDimension.getDimensionIn(direction)!=null){
			Square other = getRootDungeon().getSquareAt(absoluteDimension.getDimensionIn(direction));
		if(other!=null){
			if(other!=null){
				setBorder(square, other, direction);
				if(!square.getBorderAt(direction).getIsolation()){
					getRootDungeon().getRoomOf(other).externalMergeWith(getRootDungeon().getRoomOf(square));
				}
				
			}
		}
		}
	}
	
	/**
	 * Removes a subdungeon from this dungeon.
	 * 
	 * @param 	subdungeon
	 * 			The dungeon to remove from this dungeon.
	 * @post	The given dungeon is removed as subdungeon of this dungeon.
	 * 			| !new.dungeons.contains(subdungeon)
	 * @post	The given dungeon is terminated.
	 * 			| subdungeon.isTerminated()
	 */
	public <V extends Subdungeon<? extends Z>> void removeDungeon(V subdungeon) throws IllegalArgumentException{
		if(subdungeon==null)
			throw new IllegalArgumentException("No subdungeon has been specified");
		Object object = getDungeons();
		if(!getDungeons().containsValue((T)subdungeon))
			throw new IllegalArgumentException("Can't remove the dungeon at the given level.");
		removeBorders((T) subdungeon);
		dungeons.remove(subdungeon.getStartDimensionInContainingdungeon());
	}
	
	/**
	 *  //TODO alle subdungeons ook termineren.
	 */
	private void terminate(){
	}
	
	/**
	 * Remove the squares of the squares who are located at the border of the given dungeon.
	 * 
	 * @param 	subdungeon
	 * 			The subdungeon that gets removed.
	 * @post	All the squares that are at the border are removed.
	 * 			| there exists no dimension in new.subdungeon.getUsedDimensions()
	 * 			| 	such that the dimension is at the border of the subdungeon.
	 */
	
	private void removeBorders(T subdungeon){
		for(Dimension dimension: subdungeon.getUsedDimensions()){
			if(dimension.getXPosition()==0||dimension.getXPosition()==subdungeon.getMaxDimension().getXPosition()||dimension.getZPosition()==0||dimension.getZPosition()==subdungeon.getMaxDimension().getZPosition()||dimension.getYPosition()==0||dimension.getYPosition()==subdungeon.getMaxDimension().getYPosition()){
				subdungeon.removeSquareAt(dimension);
			}
		}
	}
	
	/**
	 * Checks whether this dungeon could fit in this dungeon at the given dimension.
	 * 
	 * @param 	dimension
	 * 			The dimension where the dungeon gets added.
	 * @param 	subdungeon
	 * 			The dungeon which is going to be added.
	 * @return	True if and only if there is enough space for the given dungeon to be added in this dungeon.
	 * 			| for each otherDimension in getAllPossibleDimensions(new Dimension(0,0,0), subdungeon.getMaxDimension())
	 * 			| 	otherDimension = otherDimension.add(dimension)
	 * 			|	if(!otherDimension.isSmallerThan(getMaxDimension()))
	 * 			|		return false;
	 * 			|	checkIsValidExtension(usedDimension)
	 * 			| result = true
	 */
	private boolean fit(Dimension dimension, T subdungeon){
		for(Dimension usedDimension: getAllPossibleDimensions(new Dimension(0,0,0), subdungeon.getMaxDimension())){
			usedDimension = usedDimension.add(dimension);
			if(!usedDimension.isSmallerThan(getMaxDimension()))
				return false;
			if(!checkIsValidExtension(usedDimension))
				return false;
		}
		return true;
	}
	
	/**
	 * Returns the dungeon that contains the given square.
	 * 
	 * @param 	square
	 * 			The square in this dungeon of which the subdungeon is wanted.
	 * @return	The dungeon that contains the dimension on which the square lies.
	 * 			| result = getDungeonAt(getDimensionOf(square))
	 */
	private T getDungeonOf(E square){
		return getSubDungeonAt(getDimensionOf(square));
	}

	/**
	 * Returns the SubDungeon at the given dimension.
	 * 
	 * @param	dimension
	 * 			The dimension to search in this dungeon.
	 * @return	The Dungeon at the given dimension.
	 * 			| result = {subdungeon | dimension.isLargerThan(subdungeon.getStartingDimension)
	 * 			|	&& dimension.subtract(subdungeon.getStartingDimension()).
	 * 			|	isSmallerThan(getDungeons().get(subdungeon.getStartingDimension()).getMaxDimension)}
	 */
	public T getSubDungeonAt(Dimension dimension){ 
		for(Dimension startDimension : getDimensions()){
			if(dimension.isLargerThan(startDimension)){
				Dimension newDimension = dimension.subtract(startDimension);
				if(newDimension.isSmallerThan(getDungeons().get(startDimension).getMaxDimension()))
					return getDungeons().get(startDimension);
			}	
		}
		return null;
	}
	
	
	/**
	 * Returns the starting dimension of a given subdungeon.
	 * 
	 * @param 	subdungeon
	 * 			The dungeon of which the starting dimension is required.
	 * @return	The starting dimension of this square.
	 * 			| for each dimension in getDimensions
	 * 			| if(getDungeons().get(dimension).equals(subdungeon))
	 * 			| result = dimension
	 */
	protected Dimension getDimensionOfDungeon(T subdungeon){
		for(Dimension dimension : getDimensions()){
			if(getDungeons().get(dimension).equals(subdungeon))
				return dimension;
		}
		return null;
	}
	
	//////////////////////////////////
	///////MANIPULATING SQUARES///////
	//////////////////////////////////
	/**
	 * Return the dimension of the given Square.
	 * 
	 * @param	square
	 * 			The square of which the dimension is required.
	 * @return	The dimension which contains this square.
	 */
	@Override
	public  Dimension getDimensionOf(E square){
		for(T dungeon : getAllSubdungeons()){
			Dimension dimension = dungeon.getDimensionOf( (Z) square);
			if(dimension != null)
				return dimension.add(getDimensionOfDungeon(dungeon));
		}
		return null;
	}
	
	/**
	 * Returns the square at the given dimension.
	 * 
	 * @param	absoluteDimension
	 * 			The dimension in this dungeon to know the square of.
	 * @return	Square that is located at the given dimension.
	 * 			| if(getSubDungeonAt(absolutedimension)!= null)
	 * 			|	result = getSubDungeonAt(absolutedimension).
	 * 			|		getSquareAt(absoluteDimension.subtract(getDimensionOfDungeon(getSubDungeonAt(absolutedimension))))
	 */
	@Override
	public E getSquareAt(Dimension absoluteDimension){
		T sub = getSubDungeonAt(absoluteDimension);
		if(sub != null)
			return sub.getSquareAt(absoluteDimension.subtract(getDimensionOfDungeon(sub)));
		return null;
	}
	

		
	/**
	 * Opens a door in the given square in the given direction.
	 * 
	 * @param 	square
	 * 			The square which contains a door.
	 * @param 	direction
	 * 			The direction in the given square where the door stands.
	 * @effect	Open the door in the subdungeon which contains  the given square.
	 * 			| getDungeonOf(square).openDoor(square,direction)
	 * @throws 	IllegalArgumentException
	 * 			The given square doesn't exist in this room.
	 * 			| getDimensionOf(square) == null
	 * @throws 	IllegalArgumentException
	 * 			There is no door in the given direction or the square isn't part of this dungeon.
	 * 			| !isDoor(square, direction)
	 */
	@Model @Override
	public void openDoor(E square, Direction direction){
		getDungeonOf(square).openDoor((Z) square,direction);
	}
	
	/**
	 * Closes a door in the given square in the given direction.
	 * 
	 * @param 	square
	 * 			The square which contains a door.
	 * @param 	direction
	 * 			The direction in the given square where the door stands.
	 * @post	The door gets closed in the dungeon which contains the given square.
	 * 			| getDungeonOf(square).closeDoor(square,direction);
	 * @throws 	IllegalArgumentException
	 * 			The given square doesn't exist in this room.
	 * 			| getDimensionOf(square) == null
	 * @throws 	IllegalArgumentException
	 * 			There is no door in the given direction or the square isn't part of this dungeon.
	 * 			| !isDoor(square, direction)
	 */
	@Model @Override
	public void closeDoor(E square, Direction direction){
		getDungeonOf(square).closeDoor( (Z) square,direction);
	}
	
	/**
	 * Removes a square at the given dimension.
	 * 
	 * @param	dimension
	 * 			The dimension on which the square must be removed.
	 * @effect	Remove in the dungeon that contains the given dimension, the square at that dimension.
	 * 			| getDungeonAt(dimension).removeSquareAt(dimension.subtract(getDimensionOfDungeon(getDungeonAt(dimension))))
	 */
	@Override
	public void removeSquareAt(Dimension dimension){
		if(getSubDungeonAt(dimension)!=null)
			getSubDungeonAt(dimension).removeSquareAt(dimension.subtract(getDimensionOfDungeon(getSubDungeonAt(dimension))));
	}
	
	/**
	 * Returns the relative dimension of the given dimension in the given dungeon.
	 * 
	 * @param 	dungeon
	 * 			The subdungeon in which the dimension is wanted.
	 * @param 	dimension
	 * 			The absolute dimension in this dungeon.
	 * @return	The relative dimension in the given dungeon, that equals the given dimension in this dungeon.
	 * 			| result = dimension.subtract(getDimensionOfDungeon(dungeon))
	 */
	private Dimension getDimensionIn(T dungeon, Dimension dimension){
		return dimension.subtract(getDimensionOfDungeon(dungeon));
	}

	
	/**
	 * Adds a square to this dungeon at the given position.
	 * 
	 * @param	square
	 * 			The square to add.
	 * @param	dimension
	 * 			The dimension where to add.
	 * @effect	The square gets added to the dungeon which contains the given dimension.
	 * 			| getDungeonAt(dimension).addSquare(square, getDimensionIn(getDungeonAt(dimension), dimension))
	 */
	@Override
	public void  addSquare(E square, Dimension dimension)throws IllegalArgumentException{
		super.addSquare(square, dimension);
		if(getSubDungeonAt(dimension)== null)
			throw new IllegalArgumentException("This dimension is not part of a dungeon.");
		getSubDungeonAt(dimension).addSquare((Z) square, getDimensionIn(getSubDungeonAt(dimension), dimension));
	}
	
	/**
	 * Checks whether the given dimension is a valid dimension.
	 * 
	 * @param	dimension
	 * 			Dimension to check.
	 * @return	True if and only if the given dungeon is a valid dimension.
	 * 			| if(getSubDungeonAt(dimension)==null)
	 *			| 	result = false
	 * 			| result = super.isValidDimension
	 */
	@Override
	protected boolean isValidDimension(Dimension dimension){
		if(getSubDungeonAt(dimension)==null)
			return false;
		return super.isValidDimension(dimension);
	}
	
	/**
	 * Return the given dimension in absolute value.
	 * 
	 * @param	dimension
	 * 			The given dimension
	 * @param	dungeon
	 * 			The previous dungeon.
	 * @return	The dimension in the given root dungeon.
	 * 			| if(!hascontainingDungeon())
	 * 			| 	result = dimension
	 * 			| result = getContainingDungeon().getRootDimension(dimension.add(getStartDimensionInContainingdungeon()))
	 */
	public Dimension getRootDimension(Dimension dimension){
        if(!hasContainingDungeon())
        	return dimension;
        return getContainingDungeon().getRootDimension(dimension.add(getStartDimensionInContainingdungeon()));

	}
	
	/**
	 * Returns a collection of all the dimension used in this dungeon.
	 */
	@Override
	public Collection<Dimension> getUsedDimensions(){
		Set<Dimension> dimensions = new HashSet<Dimension>();
		for(T dungeon: getAllSubdungeons()){
			for(Dimension dimension : dungeon.getUsedDimensions()){
				dimensions.add(dimension.add(getDimensionOfDungeon(dungeon)));
			}
		}
		return dimensions;
	}
	
	/**
	 * Returns the room that contains this square.
	 * 
	 * @param	square
	 * 			The square to know the room of.
	 * @return 	The room that contains this square.
	 * 			| getDungeonOf(square).getRoomOf(square)
	 */
	@Override
	public Room<? extends E> getRoomOf(Square square){
		return getDungeonOf((E)square).getRoomOf(square);
	}
		
	/**
	 * Check whether the given dimension is a valid extension for the expansion of the maximum dimension of a subdungeon.
	 * @param 	dimension
	 * 			The dimension to check.
	 * @return	True if and only if the given dimension is not part of any other dungeon.
	 * 			| for all dungeons in getAllSubdungeons()
	 * 			|	if(dimension.isLargerThan(getDimensionOfDungeon( subdungeon))&& dimension.isSmallerThan(subdungeon.getMaxDimension().add(getDimensionOfDungeon(subdungeon)))
	 * 			|		result = false
	 * 			| result = true
	 */
	protected boolean checkIsValidExtension(Dimension dimension){
		for(T subdungeon: getAllSubdungeons()){
			if(dimension.isLargerThan(getDimensionOfDungeon(subdungeon))&& dimension.isSmallerThan(subdungeon.getMaxDimension().add(getDimensionOfDungeon(subdungeon))))
				return false;
		}
		return true;
	}
	
	/////////////////////////
	//////EXTRA METHODS//////
	/////////////////////////
	@Override
	public String toString(){
		String returnString = "";
		for(T dungeon : getAllSubdungeons()){
			returnString += dungeon.toString();
		}
		return returnString;
	}
	
	@Override
	public Iterator<Z> iterator(final Condition filter){
		return new Iterator<Z>(){
            
            private Iterator<Z> dungeonIterator;
            private Iterator<Dungeon<? extends E>> dungeonsIterator  = getAllLevelsAndShafts().iterator();
                   
            {
                   start(); 
            }

            private void start(){
            	if(dungeonsIterator.hasNext())
                    dungeonIterator = (Iterator<Z>) dungeonsIterator.next().iterator(filter);
            }
            @Override
            public boolean hasNext() {
            	if(dungeonIterator.hasNext()){
            		return true;
            	}
            	else if(dungeonsIterator.hasNext()){
            		Boolean nextSquareBoolean = false;
            		while(!nextSquareBoolean && (dungeonsIterator.hasNext()||dungeonIterator.hasNext())){
            			if(!dungeonIterator.hasNext()){
            				dungeonIterator = (Iterator<Z>) dungeonsIterator.next().iterator(filter);
            				nextSquareBoolean = dungeonIterator.hasNext();
            			}
            			else nextSquareBoolean = dungeonIterator.hasNext();
            		}
            		return nextSquareBoolean;
            	}
            	return false;            }

            @Override
            public Z next() {
                    if(!hasNext())
                            throw new NoSuchElementException();
                    else if(dungeonIterator.hasNext()){
                    	return dungeonIterator.next();
                    }
                    else{
                    	dungeonIterator = (Iterator<Z>) dungeonsIterator.next().iterator(filter);
                    	return (Z) dungeonIterator.next();
                    }
            }
            
            @Override
            public void remove() throws UnsupportedOperationException{
                    throw new UnsupportedOperationException();
            }
            
            
    };

	}
}
